package org.gwt.grin.rebind;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.gwt.grin.client.features.InterpolatedModel;
import org.gwt.grin.client.features.Translator;
import org.gwt.grin.client.input.RCKeyEvent;
import org.gwt.grin.client.util.BgImage;
import org.gwt.grin.client.util.Color;
import org.gwt.grin.client.util.EasingEquation;
import org.gwt.grin.client.util.FontSpec;
import org.gwt.grin.client.util.PennerEasing;
import org.gwt.grin.client.util.Rectangle;
import org.gwt.grin.rebind.commands.SEActivatePartCommand;
import org.gwt.grin.rebind.commands.SEActivateSegmentCommand;
import org.gwt.grin.rebind.commands.SEResetFeatureCommand;
import org.gwt.grin.rebind.commands.SERunNamedCommand;
import org.gwt.grin.rebind.commands.SESegmentDoneCommand;
import org.gwt.grin.rebind.commands.SEShowCommand;
import org.gwt.grin.rebind.commands.SESyncDisplayCommand;
import org.gwt.grin.rebind.features.SEAssembly;
import org.gwt.grin.rebind.features.SEBin;
import org.gwt.grin.rebind.features.SEBox;
import org.gwt.grin.rebind.features.SEClipped;
import org.gwt.grin.rebind.features.SEFade;
import org.gwt.grin.rebind.features.SEFixedImage;
import org.gwt.grin.rebind.features.SEGroup;
import org.gwt.grin.rebind.features.SEImage;
import org.gwt.grin.rebind.features.SEModifier;
import org.gwt.grin.rebind.features.SEScalingModel;
import org.gwt.grin.rebind.features.SEText;
import org.gwt.grin.rebind.features.SETimer;
import org.gwt.grin.rebind.features.SETranslator;
import org.gwt.grin.rebind.features.SETranslatorModel;
import org.gwt.grin.rebind.input.SECommandRCHandler;
import org.gwt.grin.rebind.input.SERCHandler;
import org.gwt.grin.rebind.input.SEVisualRCHandler;
import org.gwt.grin.rebind.util.PointsEasingEquation;
import org.gwt.grin.rebind.util.VisualRCHandlerCell;
import org.gwt.grin.rebind.util.VisualRCHandlerHelper;

import com.google.gwt.dom.client.Style.FontStyle;
import com.google.gwt.dom.client.Style.FontWeight;
import com.google.gwt.uibinder.rebind.MortalLogger;

public class ShowParser {

	private static final int NUM_DEFERRED_LISTS = 4; // We use 4 slots

	private Lexer lexer;
	private ExtensionParser extParser;
	private ArrayList<ArrayList<ForwardReference>> deferred;

	/**
	 * A useful constant for parsing: An empty command array
	 **/
	public final static SECommand[] emptyCommandArray = new SECommand[0];

	private ShowBuilder builder;

	private Map<String, VisualRCHandlerHelper> visualRCHelpers = new HashMap<String, VisualRCHandlerHelper>();

	public ShowParser(Reader reader, String grinFile,
			ExtensionParser extParser, MortalLogger logger) {
		deferred = new ArrayList<ArrayList<ForwardReference>>();
		for (int i = 0; i < NUM_DEFERRED_LISTS; i++) {
			deferred.add(new ArrayList<ForwardReference>());
		}

		this.lexer = new Lexer(reader, grinFile, this, logger);

		if (builder == null) {
			builder = new ShowBuilder();
		}

		this.extParser = extParser;
	}

	ShowBuilder getBuilder() {
		return builder;
	}

	/**
	 * Adds a forward reference to a show feature. The forward reference will be
	 * resolved after the entire show has been read. This is useful from an
	 * extension parser for looking up other parts of a show, like a named
	 * feature. You can use it like this:
	 * 
	 * <pre>
	 * 
	 *     Lexer lexer = ...;
	 *     final ShowParser parser = lexer.getParser();
	 *     final MyFeature feature = ...;
	 *     final String otherFeatureName = ...;
	 *     ForwardReference fw = new ForwardReference(lexer) {
	 *         public void resolve() throws IOException {
	 *             Feature f = parser.lookupFeatureOrFail(otherFeatureName);
	 *             feature.setOtherFeature(f);
	 *         }
	 *     };
	 *     parser.addForwardReference(fw, 0);
	 * 
	 * </pre>
	 * 
	 * The GRIN parser guarantees that all of its forward reference resolution
	 * will be completed before the first forward reference added by this
	 * method. For example, all groups and assemblies will be completely
	 * populated before any of your forward references are.
	 * <p>
	 * Within your forward references, you might need to make sure that some of
	 * them are resolved before others. Within GRIN, and example of this is the
	 * visual RC handler, which depends on the assembly it's bound to being
	 * completely resolved before the RC handler's references are resolved. If
	 * you need to impose an ordering on the resolution order of different kinds
	 * of forward references, use the rank parameter.
	 * 
	 * @param fw
	 *            The forward reference to add
	 * @param rank
	 *            The rank order. Higher numbered forward references are
	 *            processed after lower numbered ones. Internally, the parser
	 *            uses this as an array index. Must be >= 0.
	 **/
	public void addForwardReference(ForwardReference fw, int rank) {
		while (deferred.size() <= rank + NUM_DEFERRED_LISTS) {
			deferred.add(new ArrayList<ForwardReference>());
		}
		deferred.get(rank + NUM_DEFERRED_LISTS).add(fw);
	}

	public void parse() throws IOException {
		String tok = lexer.getString();

		if (!"show".equals(tok)) {
			lexer.reportError("\"show\" expected");
		}
		// show.setDrawTargets ...

		// Parse the settings
		for (;;) {
			tok = lexer.getString();
			int lineStart = lexer.getLineNumber();
			if (!("setting".equals(tok))) {
				break;
			}
			tok = lexer.getString();
			if ("segment_stack_depth".equals(tok)) {
				parseSegmentStackDepth();
			} else {
				lexer.reportError("Unrecognized setting \"" + tok + "\".");
			}
		}

		// Parse the exports clause
		if ("exports".equals(tok)) {
			parseExpected("segments");
			String[] publicSegments = parseStrings();
			parseExpected("features");
			String[] publicFeatures = parseStrings();
			// parseExpected("handlers");
			// String[] publicHandlers = parseStrings();
			tok = lexer.getString();
			String[] publicNamedCommands;
			if ("named_commands".equals(tok)) {
				publicNamedCommands = parseStrings();
				tok = lexer.getString();
			} else {
				publicNamedCommands = new String[0];
			}
			lexer.expectString(";", tok);
			builder.setExported(publicSegments, publicFeatures,
					publicNamedCommands);
			tok = lexer.getString();
		}

		// Parse the show body
		for (;;) {
			// Current token is in tok
			int lineStart = lexer.getLineNumber();
			if (tok == null) {
				lexer.reportError("EOF unexpected");
			} else if ("end_show".equals(tok)) {
				finishBuilding();
				return;
			} else if ("segment".equals(tok)) {
				parseSegment(lineStart);
			} else if ("feature".equals(tok)) {
				parseFeature(true, lineStart);
			} else if ("named_command".equals(tok)) {
				// parseNamedCommands(lineStart);
			} else if ("rc_handler".equals(tok)) {
				tok = lexer.getString();
				if ("visual".equals(tok)) {
					parseVisualRCHandler();
				} else if ("key_pressed".equals(tok)) {
					parseCommandRCHandler(true);
				} else if ("key_released".equals(tok)) {
					parseCommandRCHandler(false);
				} else {
					lexer.reportError("Unrecognized token \"" + tok + "\"");
				}
			} else if ("show_top".equals(tok)) {
				parseShowTop();
			} else {
				lexer.reportError("Unrecognized token \"" + tok + "\"");
			}
			tok = lexer.getString();
		}
	}

	private void parseVisualRCHandler() throws IOException {
		VisualRCHandlerHelper helper = new VisualRCHandlerHelper();
		int lineStart = lexer.getLineNumber();
		String handlerName = lexer.getString();
		helper.setHandlerName(handlerName);
		visualRCHelpers.put(handlerName, helper);
		String tok = lexer.getString();
		if ("grid".equals(tok)) {
			String msg = helper.addGrid(parseVisualGrid(null));
			if (msg != null) {
				lexer.reportError(msg);
			}
			tok = lexer.getString();
			if ("rc_override".equals(tok)) {
				helper.addRCOverrides(parseRCOverride());
				tok = lexer.getString();
			} else {
				helper.addRCOverrides(new HashMap<String, String>());
			}
		} else if ("grid_alternates".equals(tok)) {
			parseExpected("{");
			for (;;) {
				tok = lexer.getString();
				if ("}".equals(tok)) {
					tok = lexer.getString();
					break;
				}
				helper.addGrid(parseVisualGrid(helper));
				helper.addGridAlternateName(tok);
			}
		} else {
			lexer.reportError("\"grid\" or \"grid_alternates\" expected, \""
					+ tok + "\" seen");
		}
		if ("assembly".equals(tok)) {
			tok = lexer.getString(); // Assembly name
			String next = lexer.getString();
			if ("start_selected".equals(next)) {
				helper.setStartSelected(lexer.getBoolean());
				next = lexer.getString();
			}
			lexer.expectString("select", next);
		} else {
			lexer.expectString("select", tok);
			tok = null; // Assembly name
		}
		final String assemblyName = tok;

		Map<String, Integer> states = helper.getStates();
		Object[] oa = parseVisualActions(states, assemblyName != null);
		final String[] selectParts = (String[]) oa[0];
		helper.setSelectCommands((SECommand[][]) oa[1]);
		parseExpected("activate");
		oa = parseVisualActions(states, assemblyName != null);
		final String[] activateParts = (String[]) oa[0];
		helper.setActivateCommands((SECommand[][]) oa[1]);

		tok = lexer.getString();
		Rectangle[] mouseRects = null;
		int[] mouseRectStates = null;
		if ("mouse".equals(tok)) {
			Vector v = parseMouseLocations(states);
			mouseRects = new Rectangle[v.size() / 2];
			mouseRectStates = new int[v.size() / 2];
			for (int i = 0; i < mouseRects.length; i++) {
				mouseRectStates[i] = ((Integer) v.elementAt(i * 2)).intValue();
				mouseRects[i] = (Rectangle) v.elementAt(i * 2 + 1);
			}
			tok = lexer.getString();
		}
		helper.setMouseRects(mouseRects);
		helper.setMouseRectStates(mouseRectStates);
		int timeout = -1;
		SECommand[] timeoutCommands = emptyCommandArray;
		if ("timeout".equals(tok)) {
			timeout = lexer.getInt();
			parseExpected("frames");
			timeoutCommands = parseCommands();
			tok = lexer.getString();
		}
		helper.setTimeout(timeout);
		helper.setTimeoutCommands(timeoutCommands);
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen");
		}
		SEVisualRCHandler handler = null;
		try {
			handler = helper.getFinishedHandler();
		} catch (IOException ex) {
			lexer.reportError(ex.getMessage());
		}
		final SEVisualRCHandler hand = handler;
		builder.addRCHandler(handlerName, lineStart, hand);
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				SEAssembly assembly = null;
				SEFeature[] realSelParts = null;
				SEFeature[] realActParts = null;
				if (assemblyName != null) {
					assembly = lookupAssemblyOrFail(assemblyName);
					realSelParts = lookupAssemblyParts(assembly, selectParts);
					realActParts = lookupAssemblyParts(assembly, activateParts);
				}
				hand.setup(assembly, realSelParts, realActParts);
			}
		};
		deferred.get(2).add(fw);
	}

	private ArrayList<ArrayList<VisualRCHandlerCell>> parseVisualGrid(
			VisualRCHandlerHelper helper) throws IOException {
		ArrayList<ArrayList<VisualRCHandlerCell>> result = new ArrayList<ArrayList<VisualRCHandlerCell>>();
		parseExpected("{");
		for (;;) {
			String tok = lexer.getString();
			if ("}".equals(tok)) {
				if (helper != null) {
					helper.addRCOverrides(new HashMap<String, String>());
				}
				break;
			} else if (helper != null && "rc_override".equals(tok)) {
				// If we have a helper, that means we're in
				// visual_grid_alternates, and therefore the rc_override
				// happens within the grid, and not outside of it.
				helper.addRCOverrides(parseRCOverride());
				parseExpected("}");
				break;
			} else if ("{".equals(tok)) {
				result.add(parseVisualGridRow());
			} else {
				lexer.reportError("'{' or '}' expected, " + tok + " seen");
			}
		}
		return result;
	}

	private ArrayList<VisualRCHandlerCell> parseVisualGridRow()
			throws IOException {
		ArrayList<VisualRCHandlerCell> result = new ArrayList<VisualRCHandlerCell>();
		for (;;) {
			String tok = lexer.getString();
			VisualRCHandlerCell cell = null;
			if (tok == null) {
				lexer.reportError("EOF unexpected in string list");
			} else if ("}".equals(tok)) {
				break;
			} else if ("(".equals(tok)) {
				int x = lexer.getInt();
				int y = lexer.getInt();
				cell = VisualRCHandlerCell.newLocationRef(x, y);
				if (cell == null) {
					lexer.reportError("Invalid cell address ( " + x + " " + y
							+ " )");
				}
				parseExpected(")");
			} else if ("[".equals(tok)) {
				String name = lexer.getString();
				parseExpected("]");
				cell = VisualRCHandlerCell.newStateRef(name);
			} else if ("<activate>".equals(tok)) {
				cell = VisualRCHandlerCell.newActivate();
			} else if ("<wall>".equals(tok)) {
				cell = VisualRCHandlerCell.newWall();
			} else if ("<null>".equals(tok)) {
				cell = VisualRCHandlerCell.newNull();
			} else {
				cell = VisualRCHandlerCell.newState(tok);
			}
			result.add(cell);
		}
		return result;
	}

	//
	// The key is a string of the form "<direction>:<statename>", as
	// in "up:close". The value is the name of the state to go to with
	// that keypress, or the special value "<activate>"
	//
	private Map<String, String> parseRCOverride() throws IOException {
		Map<String, String> result = new HashMap<String, String>();
		parseExpected("{");
		for (;;) {
			String tok = lexer.getString();
			if ("}".equals(tok)) {
				break;
			} else if (!("{".equals(tok))) {
				lexer.reportError("\"{\" expected, \"" + tok + "\" seen");
			}
			String fromState = lexer.getString();
			String direction = lexer.getString();
			String toState = lexer.getString();
			parseExpected("}");
			if (!("up".equals(direction) || "down".equals(direction)
					|| "left".equals(direction) || "right".equals(direction))) {
				lexer.reportError("Direction must be up, down, right or left, "
						+ "not \"" + direction + "\".");
			}
			result.put(direction + ":" + fromState, toState);
		}
		return result;
	}

	// Return value [0] is list of parts, [1] is list of commands lists.
	Object[] parseVisualActions(Map<String, Integer> states, boolean hasAssembly)
			throws IOException {
		String[] parts = null;
		SECommand[][] commands = null;
		parseExpected("{");
		String tok = lexer.getString();
		for (;;) {
			if ("}".equals(tok)) {
				break;
			}
			String stateName = tok;
			Integer state = states.get(stateName);
			if (state == null) {
				lexer.reportError("State " + tok + " not found");
			}
			tok = lexer.getString();
			if (!("{".equals(tok))) { // If not command list
				if (!hasAssembly) {
					lexer.reportError("No assembly specified");
				}
				if (parts == null) {
					parts = new String[states.size()];
				}
				if (parts[state.intValue()] != null) {
					lexer.reportError("State " + stateName
							+ " has duplicate assembly parts");
				}
				parts[state.intValue()] = tok;
				tok = lexer.getString();
			}
			if ("{".equals(tok)) { // If command list
				if (commands == null) {
					commands = new SECommand[states.size()][];
				}
				if (commands[state.intValue()] != null) {
					lexer.reportError("State " + stateName
							+ " has duplicate commands");
				}
				commands[state.intValue()] = parseCommandsNoOpenBrace();
				tok = lexer.getString();
			}
		}
		return new Object[] { parts, commands };
	}

	// Return value alternates Integer state # and Rectangle
	private Vector<Object> parseMouseLocations(Map<String, Integer> states)
			throws IOException {
		parseExpected("{");
		Vector<Object> result = new Vector<Object>();
		for (;;) {
			String tok = lexer.getString();
			if ("}".equals(tok)) {
				break;
			}
			Integer state = states.get(tok);
			if (state == -1) {
				lexer.reportError("State " + tok + " not found");
			}
			result.add(state);
			result.add(parseRectangle());
		}
		return result;
	}

	private void parseCommandRCHandler(boolean wantKeypress) throws IOException {
		int lineStart = lexer.getLineNumber();
		String handlerName = lexer.getString();
		int mask = parseRCKeyList();
		parseExpected("execute");
		SECommand[] commands = parseCommands();
		parseExpected(";");
		builder.addRCHandler(handlerName, lineStart, new SECommandRCHandler(
				handlerName, mask, wantKeypress, commands));
	}

	//
	// Returns the key mask of the set of keys
	//
	private int parseRCKeyList() throws IOException {
		parseExpected("{");
		int mask = 0;
		for (;;) {
			String tok = lexer.getString();
			RCKeyEvent e = null;
			if ("}".equals(tok)) {
				break;
			}
			if (tok != null) {
				e = RCKeyEvent.getByName(tok);
			}
			if (e == null) {
				lexer.reportError("Unexpected token " + tok);
			}
			mask |= e.getBitMask();
		}
		return mask;
	}

	private void parseSegmentStackDepth() throws IOException {
		int depth = lexer.getInt();
		if (depth < 0) {
			lexer.reportError("Illegal depth:  " + depth);
		}
		parseExpected(";");
		// show.setSegmentStackDepth(depth);
	}

	private void parseShowTop() throws IOException {
		final String showTopName = lexer.getString();
		parseExpected(";");
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				builder.setShowTop(showTopName);
			}
		};
		deferred.get(0).add(fw);
	}

	private void parseSegment(final int line) throws IOException {
		final String name = lexer.getString();
		String[] sa;
		String s;
		String tok = lexer.getString();
		if ("active".equals(tok)) {
			sa = parseStrings();
			tok = lexer.getString();
		} else {
			sa = new String[0];
		}
		final SubFeature[] active = makeSubFeatureList(sa);
		if ("setup".equals(tok)) {
			sa = parseStrings();
			tok = lexer.getString();
		} else {
			sa = new String[0];
		}
		final SubFeature[] setup = makeSubFeatureList(sa);
		if ("rc_handlers".equals(tok)) {
			sa = parseStrings();
			tok = lexer.getString();
		} else {
			sa = new String[0];
		}
		final String[] rcHandlers = sa;
		SECommand[] ca;
		if ("on_entry".equals(tok)) {
			ca = parseCommands();
			tok = lexer.getString();
		} else {
			ca = emptyCommandArray;
		}
		final SECommand[] onEntry = ca;
		final boolean nextOnSetupDone = "setup_done".equals(tok);
		if (nextOnSetupDone || "next".equals(tok)) {
			ca = parseCommands();
			tok = lexer.getString();
		} else {
			ca = emptyCommandArray;
		}
		final SECommand[] next = ca;
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen");
		}
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				SEFeature[] a = makeFeatureList(active);
				SEFeature[] s = makeFeatureList(setup);
				SERCHandler[] h = makeRCHandlerList(rcHandlers);
				SESegment segment = new SESegment(name, a, s, h, onEntry,
						nextOnSetupDone, next);
				builder.addSegment(name, line, segment);
			}
		};
		deferred.get(0).add(fw);
	}

	/**
	 * Parse a list of commands.
	 * 
	 * @return
	 * @throws IOException
	 */
	public SECommand[] parseCommands() throws IOException {
		parseExpected("{");
		return parseCommandsNoOpenBrace();
	}

	/**
	 * Parse a list of commands after the leading "{" has already been read.
	 * 
	 * @return
	 * @throws IOException
	 */
	public SECommand[] parseCommandsNoOpenBrace() throws IOException {
		List<SECommand> list = new ArrayList<SECommand>();
		for (;;) {
			String tok = lexer.getString();
			int lineStart = lexer.getLineNumber();
			if ("}".equals(tok)) {
				break;
			} else {
				SECommand c = parseCommand(tok);
				list.add(c);
				builder.addCommand(c, lineStart);
			}
		}
		int num = list.size();
		SECommand[] result = new SECommand[num];
		list.toArray(result);
		return result;
	}

	/*
	 * Parse a command. The first token is passed in as argument, and remaining
	 * token are pulled from the lexer.
	 */
	private SECommand parseCommand(String tok) throws IOException {
		if ("activate_segment".equals(tok)) {
			return parseActivateSegment();
		} else if ("activate_part".equals(tok)) {
			return parseActivatePart();
		} else if ("segment_done".equals(tok)) {
			return parseSegmentDone();
			// } else if ("set_visual_rc".equals(tok)) {
			// return parseVisualRC();
		} else if ("reset_feature".equals(tok)) {
			return parseResetFeature();
		} else if ("sync_display".equals(tok)) {
			return parseSyncDisplay();
		} else if ("run_named_commands".equals(tok)) {
			return parseNamedCommands();
		} else if ("java_command".equals(tok)) {
			return parseJavaCommand();
		} else if (extParser == null || tok == null || tok.indexOf(':') < 0) {
			lexer.reportError("command expected, " + tok + " seen");
			return null;
		}
		// else {
		// String typeName = tok;
		// SECommand result = extParser.getCommand(show, typeName, lexer);
		// if (result == null) {
		// lexer.reportError("command expected, " + tok + " seen");
		// }
		// return result;
		// }
		return null;
	}

	private SECommand parseJavaCommand() throws IOException {
		parseExpected("[[");
		StringBuffer javaSource = new StringBuffer();
		readJavaSource(javaSource);
		SEShowCommand result = new SEShowCommand(javaSource.toString());
		return result;
	}

	private SECommand parseNamedCommands() throws IOException {
		final String name = lexer.getString();
		parseExpected(";");
		final SERunNamedCommand cmd = new SERunNamedCommand();
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				SECommand target = builder.getNamedCommand(name);
				if (target == null) {
					reportError("Can't find named command \"" + name + "\"");
				}
				cmd.setup(target);
			}
		};
		deferred.get(0).add(fw);
		return cmd;
	}

	private SECommand parseSyncDisplay() throws IOException {
		parseExpected(";");
		return new SESyncDisplayCommand();
	}

	private SECommand parseResetFeature() throws IOException {
		final String featureName = lexer.getString();
		parseExpected(";");
		final SEResetFeatureCommand cmd = new SEResetFeatureCommand();
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				SEFeature f = lookupFeatureOrFail(featureName);
				cmd.setup(f);
			}
		};
		deferred.get(0).add(fw);
		return cmd;
	}

	private SECommand parseSegmentDone() throws IOException {
		parseExpected(";");
		return new SESegmentDoneCommand();
	}

	private SECommand parseActivatePart() throws IOException {
		final String assemblyName = lexer.getString();
		final String partName = lexer.getString();
		parseExpected(";");
		final SEActivatePartCommand cmd = new SEActivatePartCommand();
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				SEAssembly a = lookupAssemblyOrFail(assemblyName);
				SEFeature f = a.findPart(partName);
				if (f == null) {
					reportError("Assembly part \"" + partName + " not found");
				}
				cmd.setup(a, f);
			}
		};
		deferred.get(3).add(fw);
		return cmd;
	}

	private SECommand parseActivateSegment() throws IOException {
		final String name = lexer.getString();
		final boolean pop = "<pop>".equals(name);
		String tok = lexer.getString();
		final boolean push = !pop && "<push>".equals(tok);
		if (push) {
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen");
		}
		final SEActivateSegmentCommand cmd = new SEActivateSegmentCommand(push,
				pop);
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				if (!pop) {
					SESegment s = builder.getNamedSegment(name);
					if (s == null) {
						reportError("Segment \"" + name + " not found");
					} else {
						cmd.setSegment(s);
					}
				}
				// if (push || pop) {
				// if (show.getSegmentStackDepth() <= 0) {
				// reportError("Segment stack depth is 0");
				// }
				// }
			}
		};
		deferred.get(3).add(fw);
		return cmd;
	}

	//
	// Segments only have named subfeatures, so this works.
	//
	private SubFeature[] makeSubFeatureList(String[] names) {
		SubFeature[] result = new SubFeature[names.length];
		for (int i = 0; i < names.length; i++) {
			result[i] = new SubFeature(builder, names[i], lexer);
		}
		return result;
	}

	private SEFeature parseFeature(boolean hasName, int lineStart)
			throws IOException {
		String tok = lexer.getString();
		if ("image".equals(tok)) {
			return parseImage(hasName, lineStart);
		} else if ("fixed_image".equals(tok)) {
			return parseFixedImage(hasName, lineStart);
		} else if ("image_sequence".equals(tok)) {
			// return parseImageSequence(hasName, lineStart);
			return null;
		} else if ("bin".equals(tok)) {
			return parseBin(hasName, lineStart);
		} else if ("box".equals(tok)) {
			return parseBox(hasName, lineStart);
		} else if ("assembly".equals(tok)) {
			return parseAssembly(hasName, lineStart);
		} else if ("menu_assembly".equals(tok)) {
			// return parseMenuAssembly(hasName, lineStart);
			return null;
		} else if ("group".equals(tok)) {
			return parseGroup(hasName, lineStart);
		} else if ("clipped".equals(tok)) {
			return parseClipped(hasName, lineStart);
		} else if ("src_over".equals(tok)) {
			// return parseSrcOver(hasName, lineStart);
			return null;
		} else if ("fade".equals(tok)) {
			return parseFade(hasName, lineStart);
		} else if ("timer".equals(tok)) {
			return parseTimer(hasName, lineStart);
		} else if ("translation".equals(tok) || "translator_model".equals(tok)) {
			return parseTranslatorModel(hasName, lineStart);
		} else if ("translator".equals(tok)) {
			return parseTranslator(hasName, lineStart);
		} else if ("text".equals(tok)) {
			return parseText(hasName, lineStart);
		} else if ("scaling_model".equals(tok)) {
			return parseScalingModel(hasName, lineStart);
		} else if ("guarantee_fill".equals(tok)) {
			// return parseGuaranteeFill(hasName, lineStart);
			return null;
		} else if ("set_target".equals(tok)) {
			// return parseSetTarget(hasName, lineStart);
			return null;
		} else if ("showtop_group".equals(tok)) {
			// return parseShowTopGroup(hasName, lineStart);
			return null;
		} else if (extParser == null || tok == null) {
			lexer.reportError("Unrecognized feature \"" + tok + "\"");
			return null; // not reached
		} else if ("extension".equals(tok) || "modifier".equals(tok)) {
			return null;// TODO;
		} else {
			lexer.reportError("Unrecognized feature \"" + tok + "\"");
			return null; // not reached
		}
	}

	private SEFeature parseTimer(boolean hasName, int line) throws IOException {
		String name = parseFeatureName(hasName);
		int numFrames = lexer.getInt();
		String tok = lexer.getString();
		boolean repeat = false;
		if ("repeat".equals(tok)) {
			repeat = true;
			parseExpected("{");
		} else if ("{".equals(tok)) {
			// do nothing, i.e. consume token
		} else {
			lexer.reportError("'{' or 'repeat' expected");
		}
		SECommand[] commands = parseCommandsNoOpenBrace();
		parseExpected(";");
		if (numFrames < 0 || (repeat && numFrames < 1)) {
			lexer.reportError("More frames, please.");
		}
		SETimer timer = new SETimer(name, numFrames, repeat, commands);
		builder.addFeature(name, line, timer);
		return timer;
	}

	private SEFeature parseAssembly(boolean hasName, int line)
			throws IOException {
		String name = parseFeatureName(hasName);
		ArrayList<String> namesList = new ArrayList<String>();
		ArrayList<SubFeature> partsList = new ArrayList<SubFeature>();
		parseExpected("{");
		for (;;) {
			String tok = lexer.getString();
			if (tok == null) {
				lexer.reportError("Unexpected EOF");
			} else if ("}".equals(tok)) {
				break;
			}
			namesList.add(tok);
			tok = lexer.getString();
			partsList.add(parseSubFeature(tok));
		}
		parseExpected(";");
		final SEAssembly a = new SEAssembly(name);
		builder.addFeature(name, line, a);
		final String[] names = namesList.toArray(new String[namesList.size()]);
		final SubFeature[] parts = partsList.toArray(new SubFeature[partsList
				.size()]);
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				a.setup(names, makeFeatureList(parts));
			}
		};
		deferred.get(0).add(fw);
		return a;
	}

	private SEFeature parseGroup(boolean hasName, int line) throws IOException {
		String name = parseFeatureName(hasName);
		final SubFeature[] parts = parsePartsList();
		parseExpected(";");
		final SEGroup group = new SEGroup(name);
		builder.addFeature(name, line, group);
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				group.setup(makeFeatureList(parts));
			}
		};
		deferred.get(0).add(fw);
		return group;
	}

	private SEFeature parseScalingModel(boolean hasName, int line)
			throws IOException {
		final String name = parseFeatureName(hasName);
		parseExpected("{");
		ArrayList<int[]> keyframes = new ArrayList<int[]>();
		for (;;) {
			String tok = lexer.getString();
			if ("}".equals(tok)) {
				break;
			}
			int frameNum = 0;
			try {
				frameNum = Integer.decode(tok).intValue();
			} catch (NumberFormatException ex) {
				lexer.reportError(ex.toString());
			}
			int x = lexer.getInt();
			int y = lexer.getInt();
			int scaleX = lexer.getInt();
			int scaleY = lexer.getInt();
			tok = lexer.getString();
			EasingEquation[] easing = new EasingEquation[1];
			if (!("mills".equals(tok))) {
				tok = parseEasing(tok, easing);
			}
			if (keyframes.size() == 0 || easing[0] == null) {
				keyframes.add(new int[] { frameNum, x, y, scaleX, scaleY });
			} else {
				int[] destination = { frameNum, x, y, scaleX, scaleY };
				try {
					easing[0].addKeyFrames(keyframes, destination);
				} catch (Exception ex) {
					lexer.reportError("Easing:  " + ex.getMessage());
				}
			}
			lexer.expectString("mills", tok);
		}
		String tok = lexer.getString();
		int repeatFrame = -1;
		if ("repeat".equals(tok)) {
			repeatFrame = lexer.getInt();
			tok = lexer.getString();
		}
		int loopCount = 1;
		if ("loop_count".equals(tok)) {
			loopCount = lexer.getIntOrInfinite();
			tok = lexer.getString();
		}
		boolean dynScaleX = false;
		if ("dyn_scale_x".equals(tok)) {
			dynScaleX = true;
			tok = lexer.getString();
		}
		boolean dynScaleY = false;
		if ("dyn_scale_y".equals(tok)) {
			dynScaleY = true;
			tok = lexer.getString();
		}
		SECommand[] endCommands = emptyCommandArray;
		if ("end_commands".equals(tok)) {
			endCommands = parseCommands();
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen");
		}
		int[] fs = new int[keyframes.size()];
		int[][] values = new int[4][];
		values[0] = new int[keyframes.size()];
		values[1] = new int[keyframes.size()];
		values[2] = new int[keyframes.size()];
		values[3] = new int[keyframes.size()];
		for (int i = 0; i < keyframes.size(); i++) {
			int[] el = keyframes.get(i);
			fs[i] = el[0];
			values[InterpolatedModel.SCALE_X_FIELD][i] = el[1];
			values[InterpolatedModel.SCALE_Y_FIELD][i] = el[2];
			values[InterpolatedModel.SCALE_X_FACTOR_FIELD][i] = el[3];
			values[InterpolatedModel.SCALE_Y_FACTOR_FIELD][i] = el[4];
			if (i > 0 && fs[i] <= fs[i - 1]) {
				lexer.reportError("Frame number must be increasing");
			}
		}
		if (fs.length < 1) {
			lexer.reportError("Need at least one keyframe");
		}
		if (fs[0] != 0) {
			lexer.reportError("Keyframes must start at frame 0");
		}
		if (repeatFrame == -1) {
			repeatFrame = Integer.MAX_VALUE; // Make it stick at end
		} else if (repeatFrame > fs[fs.length - 1]) {
			lexer.reportError("repeat > max frame");
		}
		SEScalingModel scaleModel = new SEScalingModel(name, fs, values,
				repeatFrame, loopCount, endCommands, dynScaleX, dynScaleY);
		builder.addFeature(name, line, scaleModel);
		return scaleModel;
	}

	private SEFeature parseFixedImage(boolean hasName, int line)
			throws IOException {
		final String name = parseFeatureName(hasName);
		Rectangle placement = parseRectangle();
		String imageResource = lexer.getString();
		String tok = lexer.getString();
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen!");
		}
		final SEFixedImage box = new SEFixedImage(name, placement.x,
				placement.y, imageResource);
		builder.addFeature(name, line, box);
		// TODO scaling model...
		return box;
	}

	private SEFeature parseBin(boolean hasName, int lineStart)
			throws IOException {
		final String name = parseFeatureName(hasName);
		Rectangle placement = parseRectangle();
		String tok = lexer.getString();
		int outlineWidth = 0;
		Color outlineColor = null;
		if ("outline".equals(tok)) {
			outlineWidth = lexer.getInt();
			if (outlineWidth < 1) {
				lexer.reportError("" + outlineWidth + " is an illegal width!");
			}
			if (outlineWidth * 2 > placement.width
					|| outlineWidth * 2 > placement.height) {
				lexer.reportError("Outline too wide for box size!");
			}
			outlineColor = parseColor();
			tok = lexer.getString();
		}
		Color fillColor = null;
		if ("fill".equals(tok)) {
			fillColor = parseColor();
			tok = lexer.getString();
		}
		BgImage bgImage = parseBgImage(tok);
		if (bgImage != null) {
			tok = lexer.getString();
		}
		String scalingModel = null;
		if ("scaling_model".equals(tok)) {
			scalingModel = lexer.getString();
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen!");
		}
		final SEBin bin = new SEBin(name, placement.x, placement.y,
				placement.width, placement.height, outlineWidth, outlineColor,
				fillColor, bgImage);
		builder.addFeature(name, lineStart, bin);
		if (scalingModel != null) {
			final String scalingModelF = scalingModel;
			ForwardReference fw = new ForwardReference(lexer) {
				@Override
				public void resolve() throws IOException {
					SEFeature smf = builder.getNamedFeature(scalingModelF);
					if (smf == null || !(smf instanceof SEScalingModel)) {
						lexer.reportError("In box " + name
								+ " can't find scale_model " + scalingModelF
								+ ".");
					}
					bin.setup((SEScalingModel) smf);
				}
			};
			deferred.get(0).add(fw);
		}
		return bin;
	}

	private SEFeature parseBox(boolean hasName, int lineStart)
			throws IOException {
		final String name = parseFeatureName(hasName);
		Rectangle placement = parseRectangle();
		String tok = lexer.getString();
		int outlineWidth = 0;
		Color outlineColor = null;
		if ("outline".equals(tok)) {
			outlineWidth = lexer.getInt();
			if (outlineWidth < 1) {
				lexer.reportError("" + outlineWidth + " is an illegal width!");
			}
			if (outlineWidth * 2 > placement.width
					|| outlineWidth * 2 > placement.height) {
				lexer.reportError("Outline too wide for box size!");
			}
			outlineColor = parseColor();
			tok = lexer.getString();
		}
		Color fillColor = null;
		if ("fill".equals(tok)) {
			fillColor = parseColor();
			tok = lexer.getString();
		}
		BgImage bgImage = parseBgImage(tok);
		if (bgImage != null) {
			tok = lexer.getString();
		}
		String scalingModel = null;
		if ("scaling_model".equals(tok)) {
			scalingModel = lexer.getString();
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen!");
		}
		final SEBox box = new SEBox(name, placement.x, placement.y,
				placement.width, placement.height, outlineWidth, outlineColor,
				fillColor, bgImage);
		builder.addFeature(name, lineStart, box);
		if (scalingModel != null) {
			final String scalingModelF = scalingModel;
			ForwardReference fw = new ForwardReference(lexer) {
				@Override
				public void resolve() throws IOException {
					SEFeature smf = builder.getNamedFeature(scalingModelF);
					if (smf == null || !(smf instanceof SEScalingModel)) {
						lexer.reportError("In box " + name
								+ " can't find scale_model " + scalingModelF
								+ ".");
					}
					box.setup((SEScalingModel) smf);
				}
			};
			deferred.get(0).add(fw);
		}
		return box;
	}

	private SEFeature parseText(boolean hasName, int lineStart)
			throws IOException {
		final String name = parseFeatureName(hasName);
		Rectangle placement = parseRectangle();
		String text = lexer.getString();
		String tok = lexer.getString();
		int textAlign = SEText.TEXT_ALIGN_DEFAULT;
		if ("align".equals(tok)) {
			tok = lexer.getString();
			if ("left".equals(tok)) {
				textAlign = SEText.TEXT_ALIGN_LEFT;
			} else if ("right".equals(tok)) {
				textAlign = SEText.TEXT_ALIGN_RIGHT;
			} else if ("center".equals(tok)) {
				textAlign = SEText.TEXT_ALIGN_CENTER;
			} else if ("default".equals(tok)) {
				textAlign = SEText.TEXT_ALIGN_DEFAULT;
			} else {
				lexer.reportError("Text align must be specified");
			}
			tok = lexer.getString();
		}
		boolean wordWrap = true;
		if ("no-wrap".equals(tok)) {
			wordWrap = false;
			tok = lexer.getString();
		}
		FontSpec fontSpec = parseFontSpec(tok);
		tok = lexer.getString();
		int lineHeight = SEText.DEFAULT_LINE_HEIGHT;
		if ("line_height".equals(tok)) {
			lineHeight = lexer.getInt();
			tok = lexer.getString();
		}

		// TODO color

		int outlineWidth = 0;
		Color outlineColor = null;
		if ("outline".equals(tok)) {
			outlineWidth = lexer.getInt();
			if (outlineWidth < 1) {
				lexer.reportError("" + outlineWidth + " is an illegal width!");
			}
			if (outlineWidth * 2 > placement.width
					|| outlineWidth * 2 > placement.height) {
				lexer.reportError("Outline too wide for box size!");
			}
			outlineColor = parseColor();
			tok = lexer.getString();
		}
		Color fillColor = null;
		if ("fill".equals(tok)) {
			fillColor = parseColor();
			tok = lexer.getString();
		}
		BgImage bgImage = parseBgImage(tok);
		if (bgImage != null) {
			tok = lexer.getString();
		}
		String scalingModel = null;
		if ("scaling_model".equals(tok)) {
			scalingModel = lexer.getString();
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen!");
		}
		final SEText box = new SEText(name, placement.x, placement.y,
				placement.width, placement.height, text, wordWrap, fontSpec,
				outlineWidth, outlineColor, fillColor, null, textAlign,
				lineHeight);
		builder.addFeature(name, lineStart, box);
		if (scalingModel != null) {
			final String scalingModelF = scalingModel;
			ForwardReference fw = new ForwardReference(lexer) {
				@Override
				public void resolve() throws IOException {
					SEFeature smf = builder.getNamedFeature(scalingModelF);
					if (smf == null || !(smf instanceof SEScalingModel)) {
						lexer.reportError("In box " + name
								+ " can't find scale_model " + scalingModelF
								+ ".");
					}
					box.setup((SEScalingModel) smf);
				}
			};
			deferred.get(0).add(fw);
		}
		return box;
	}

	private SEFeature parseImage(boolean hasName, int lineStart)
			throws IOException {
		final String name = parseFeatureName(hasName);
		Rectangle placement = parseRectangle();
		String url = lexer.getString();
		String tok = lexer.getString();
		int outlineWidth = 0;
		Color outlineColor = null;
		if ("outline".equals(tok)) {
			outlineWidth = lexer.getInt();
			if (outlineWidth < 1) {
				lexer.reportError("" + outlineWidth + " is an illegal width!");
			}
			if (outlineWidth * 2 > placement.width
					|| outlineWidth * 2 > placement.height) {
				lexer.reportError("Outline too wide for box size!");
			}
			outlineColor = parseColor();
			tok = lexer.getString();
		}
		Color fillColor = null;
		if ("fill".equals(tok)) {
			fillColor = parseColor();
			tok = lexer.getString();
		}
		BgImage bgImage = parseBgImage(tok);
		if (bgImage != null) {
			tok = lexer.getString();
		}
		String scalingModel = null;
		if ("scaling_model".equals(tok)) {
			scalingModel = lexer.getString();
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen!");
		}
		final SEImage img = new SEImage(name, placement.x, placement.y,
				placement.width, placement.height, url, outlineWidth,
				outlineColor, fillColor, null);
		builder.addFeature(name, lineStart, img);
		if (scalingModel != null) {
			final String scalingModelF = scalingModel;
			ForwardReference fw = new ForwardReference(lexer) {
				@Override
				public void resolve() throws IOException {
					SEFeature smf = builder.getNamedFeature(scalingModelF);
					if (smf == null || !(smf instanceof SEScalingModel)) {
						lexer.reportError("In box " + name
								+ " can't find scale_model " + scalingModelF
								+ ".");
					}
					img.setup((SEScalingModel) smf);
				}
			};
			deferred.get(0).add(fw);
		}
		return img;
	}

	private SEFeature parseFade(boolean hasName, int line) throws IOException {
		final String name = parseFeatureName(hasName);
		SubFeature part = parseSubFeature(lexer.getString());
		String tok = lexer.getString();
		if (!("{".equals(tok))) {
			lexer.reportError("'{' expected, \"" + tok + "\" seen.");
		}
		ArrayList<int[]> keyframes = new ArrayList<int[]>();
		tok = lexer.getString();
		for (;;) {
			if ("}".equals(tok)) {
				break;
			}
			int frameNum = 0;
			try {
				frameNum = Integer.decode(tok).intValue();
			} catch (NumberFormatException ex) {
				lexer.reportError(ex.toString());
			}
			int alpha = lexer.getInt();
			EasingEquation[] easing = new EasingEquation[1];
			tok = lexer.getString();
			tok = parseEasing(tok, easing);
			if (keyframes.size() == 0 || easing[0] == null) {
				keyframes.add(new int[] { frameNum, alpha });
			} else {
				int[] destination = { frameNum, alpha };
				try {
					easing[0].addKeyFrames(keyframes, destination);
				} catch (Exception ex) {
					lexer.reportError("Easing:  " + ex.getMessage());
				}
			}
		}
		tok = lexer.getString();
		SECommand[] endCommands = emptyCommandArray;
		int repeatFrame;
		if ("repeat".equals(tok)) {
			repeatFrame = lexer.getInt();
			tok = lexer.getString();
		} else {
			repeatFrame = Integer.MAX_VALUE; // Off the end
		}
		int loopCount = 1;
		if ("loop_count".equals(tok)) {
			loopCount = lexer.getIntOrInfinite();
			tok = lexer.getString();
		}
		if ("end_commands".equals(tok)) {
			endCommands = parseCommands();
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen");
		}
		int[] fs = new int[keyframes.size()];
		int[] alphas = new int[keyframes.size()];
		for (int i = 0; i < keyframes.size(); i++) {
			int[] el = keyframes.get(i);
			fs[i] = el[0];
			alphas[i] = el[1];
			if (i > 0 && fs[i] <= fs[i - 1]) {
				lexer.reportError("Frame number must be increasing");
			}
			if (alphas[i] < 0 || alphas[i] > 255) {
				lexer.reportError("Illegal alpha value:  " + alphas[i]);
			}
		}
		if (fs.length < 1) {
			lexer.reportError("Need at least one keyframe");
		}
		if (fs[0] != 0) {
			lexer.reportError("Keyframes must start at frame 0");
		}
		SEFade fade = new SEFade(name, fs, alphas, repeatFrame, loopCount,
				endCommands);
		builder.addFeature(null, line, fade.getModel());
		builder.addFeature(name, line, fade);
		resolveModifier(fade, part);
		return fade;
	}

	private SEFeature parseClipped(boolean hasName, int line)
			throws IOException {
		String name = parseFeatureName(hasName);
		SubFeature part = parseSubFeature(lexer.getString());
		Rectangle clipRegion = parseRectangle();
		parseExpected(";");
		SEClipped clipped = new SEClipped(name, clipRegion);
		builder.addFeature(name, line, clipped);
		resolveModifier(clipped, part);
		return clipped;
	}

	private SEFeature parseTranslatorModel(boolean hasName, int line)
			throws IOException {
		String name = parseFeatureName(hasName);
		parseExpected("{");
		ArrayList<int[]> keyFrames = new ArrayList<int[]>();
		String tok = lexer.getString();
		for (;;) {
			if ("}".equals(tok)) {
				tok = lexer.getString();
				break;
			}
			int frameNum = 0;
			try {
				frameNum = Integer.decode(tok).intValue();
			} catch (NumberFormatException ex) {
				lexer.reportError(ex.toString());
			}
			int x = lexer.getIntOrOffscreen();
			int y = lexer.getIntOrOffscreen();
			String tweenType = lexer.getString();
			EasingEquation[] easing = new EasingEquation[1];
			if ("linear-relative".equals(tweenType)) {
				tweenType = "linear";
			}
			tok = parseEasing(tweenType, easing);
			if (keyFrames.size() == 0 || easing[0] == null) {
				keyFrames.add(new int[] { frameNum, x, y });
			} else {
				int[] destination = { frameNum, x, y };
				try {
					easing[0].addKeyFrames(keyFrames, destination);
				} catch (Exception ex) {
					lexer.reportError("Easing:  " + ex.getMessage());
				}
			}
		}
		int repeatFrame = -1;
		if ("repeat".equals(tok)) {
			repeatFrame = lexer.getInt();
			tok = lexer.getString();
		}
		int loopCount = 1;
		if ("loop_count".equals(tok)) {
			loopCount = lexer.getIntOrInfinite();
			tok = lexer.getString();
		}
		SECommand[] endCommands = emptyCommandArray;
		if ("end_commands".equals(tok)) {
			endCommands = parseCommands();
			tok = lexer.getString();
		}
		if (!(";".equals(tok))) {
			lexer.reportError("\";\" expected, \"" + tok + "\" seen");
		}
		int[] fs = new int[keyFrames.size()];
		int[][] values = new int[2][];
		values[0] = new int[keyFrames.size()];
		values[1] = new int[keyFrames.size()];
		for (int i = 0; i < keyFrames.size(); i++) {
			int[] el = keyFrames.get(i);
			fs[i] = el[0];
			values[Translator.X_FIELD][i] = el[1];
			values[Translator.Y_FIELD][i] = el[2];
			if (i > 0 && fs[i] <= fs[i - 1]) {
				lexer.reportError("Frame number must be increasing");
			}
		}
		if (fs.length < 1) {
			lexer.reportError("Need at least one keyframe");
		}
		if (fs[0] != 0) {
			lexer.reportError("Keyframes must start at frame 0");
		}
		if (repeatFrame == -1) {
			repeatFrame = Integer.MAX_VALUE; // Make it stick at end
		} else if (repeatFrame > fs[fs.length - 1]) {
			lexer.reportError("repeat > max frame");
		}
		SETranslatorModel trans = new SETranslatorModel(name, fs, values,
				repeatFrame, loopCount, endCommands);
		builder.addFeature(name, line, trans);
		return trans;
	}

	private SEFeature parseTranslator(boolean hasName, int line)
			throws IOException {
		String name = parseFeatureName(hasName);
		final String translationName = lexer.getString();
		final SubFeature[] parts = parsePartsList();
		parseExpected(";");
		final SETranslator trans = new SETranslator(name);
		builder.addFeature(name, line, trans);
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				SEFeature t = builder.getNamedFeature(translationName);
				if (t == null || !(t instanceof SETranslatorModel)) {
					lexer.reportError("Translation \"" + translationName
							+ "\" not found");
				}
				SETranslatorModel m = (SETranslatorModel) t;
				SEFeature[] fa = makeFeatureList(parts);
				if (fa.length == 1) {
					trans.setup(m, fa[0]);
				} else {
					SEGroup group = new SEGroup(null);
					group.setup(fa);
					trans.setup(m, group);
					builder.addFeature(null, 0, group);
				}
			}
		};
		deferred.get(0).add(fw);
		// builder.addDeferredBuilder(new TranslatorHelper(trans, line));
		return trans;
	}

	/*
	 * Parsethe easing, and set easing[0] to that value. Return the next token
	 * to be parsed.
	 */
	private String parseEasing(String tweenType, EasingEquation[] easing)
			throws IOException {
		String tok = lexer.getString();

		if ("linear".equals(tweenType)) {
			// Nothing special needed
		} else if ("start".equals(tweenType)) {
			// start is a synonym for linear-relative
		} else if ("ease-in-quad".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInQuad(t, b, c, d);
				}
			};
		} else if ("ease-out-quad".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutQuad(t, b, c, d);
				}
			};
		} else if ("ease-in-out-quad".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutQuad(t, b, c, d);
				}
			};
		} else if ("ease-in-cubic".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInCubic(t, b, c, d);
				}
			};
		} else if ("ease-out-cubic".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutCubic(t, b, c, d);
				}
			};
		} else if ("ease-in-out-cubic".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutCubic(t, b, c, d);
				}
			};
		} else if ("ease-in-quart".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInQuart(t, b, c, d);
				}
			};
		} else if ("ease-out-quart".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutQuart(t, b, c, d);
				}
			};
		} else if ("ease-in-out-quart".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutQuart(t, b, c, d);
				}
			};
		} else if ("ease-in-quint".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInQuint(t, b, c, d);
				}
			};
		} else if ("ease-out-quint".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutQuint(t, b, c, d);
				}
			};
		} else if ("ease-in-out-quint".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutQuint(t, b, c, d);
				}
			};
		} else if ("ease-in-sine".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInSine(t, b, c, d);
				}
			};
		} else if ("ease-out-sine".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutSine(t, b, c, d);
				}
			};
		} else if ("ease-in-out-sine".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutSine(t, b, c, d);
				}
			};
		} else if ("ease-in-expo".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInExpo(t, b, c, d);
				}
			};
		} else if ("ease-out-expo".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutExpo(t, b, c, d);
				}
			};
		} else if ("ease-in-out-expo".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutExpo(t, b, c, d);
				}
			};
		} else if ("ease-in-circ".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInCirc(t, b, c, d);
				}
			};
		} else if ("ease-out-circ".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutCirc(t, b, c, d);
				}
			};
		} else if ("ease-in-out-circ".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutCirc(t, b, c, d);
				}
			};
		} else if ("ease-in-elastic".equals(tweenType)) {
			final double[] a = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "amplitude", a);
			final double[] p = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "period", p);
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInElastic(t, b, c, d, a[0], p[0]);
				}
			};
		} else if ("ease-out-elastic".equals(tweenType)) {
			final double[] a = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "amplitude", a);
			final double[] p = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "period", p);
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutElastic(t, b, c, d, a[0], p[0]);
				}
			};
		} else if ("ease-in-out-elastic".equals(tweenType)) {
			final double[] a = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "amplitude", a);
			final double[] p = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "period", p);
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing
							.easeInOutElastic(t, b, c, d, a[0], p[0]);
				}
			};
		} else if ("ease-in-back".equals(tweenType)) {
			final double[] s = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "overshoot", s);
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInBack(t, b, c, d, s[0]);
				}
			};
		} else if ("ease-out-back".equals(tweenType)) {
			final double[] s = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "overshoot", s);
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutBack(t, b, c, d, s[0]);
				}
			};
		} else if ("ease-in-out-back".equals(tweenType)) {
			final double[] s = { PennerEasing.DEFAULT };
			tok = getOptionalParameter(tok, "overshoot", s);
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutBack(t, b, c, d, s[0]);
				}
			};
		} else if ("ease-in-bounce".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInBounce(t, b, c, d);
				}
			};
		} else if ("ease-out-bounce".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeOutBounce(t, b, c, d);
				}
			};
		} else if ("ease-in-out-bounce".equals(tweenType)) {
			easing[0] = new EasingEquation() {
				public double evaluate(double t, double b, double c, double d) {
					return PennerEasing.easeInOutBounce(t, b, c, d);
				}
			};
		} else if ("ease-points".equals(tweenType)) {
			lexer.expectString("{", tok);
			easing[0] = parseEasePoints();
			tok = lexer.getString();
		} else {
			lexer.reportError("unknown tween type \"" + tweenType + "\"");
		}
		if ("max-error".equals(tok)) {
			int maxError = lexer.getInt();
			tok = lexer.getString();
			if (easing[0] == null) {
				// It's OK to specify a max-error for linear interpolation,
				// but it doesn't mean anything, since there's no
				// approximation with linear interpolation.
			} else {
				easing[0].setMaxError(maxError);
			}
		}
		return tok;
	}

	private EasingEquation parseEasePoints() throws IOException {
		ArrayList<int[]> frames = new ArrayList<int[]>();
		for (;;) {
			String tok = lexer.getString();
			if ("}".equals(tok)) {
				break;
			} else if (!("(".equals(tok))) {
				lexer.reportError("\"(\" expected");
			}
			ArrayList<Integer> values = new ArrayList<Integer>();
			for (;;) {
				tok = lexer.getString();
				if (")".equals(tok)) {
					break;
				}
				values.add(new Integer(lexer.convertToInt(tok)));
			}
			int[] arr = new int[values.size()];
			for (int i = 0; i < values.size(); i++) {
				arr[i] = values.get(i).intValue();
			}
			frames.add(arr);
		}
		int[][] points = frames.toArray(new int[frames.size()][]);
		return new PointsEasingEquation(points);
	}

	//
	// Get an optional parameter named name. Returns the next token after
	// the optional parameter. Delivers result to value[0].
	//
	private String getOptionalParameter(String tok, String name, double[] value)
			throws IOException {
		if (name.equals(tok)) {
			value[0] = lexer.getDouble();
			return lexer.getString();
		} else {
			return tok;
		}
	}

	private FontSpec parseFontSpec(String tok) throws IOException {
		String name = tok;
		FontStyle style = FontStyle.NORMAL;
		FontWeight weight = FontWeight.NORMAL;
		int size = 14;
		tok = lexer.getString();
		if ("plain".equals(tok)) {
			style = FontStyle.NORMAL;
			weight = FontWeight.NORMAL;
		} else if ("bold".equals(tok)) {
			style = FontStyle.NORMAL;
			weight = FontWeight.BOLD;
		} else if ("italic".equals(tok)) {
			style = FontStyle.ITALIC;
			weight = FontWeight.NORMAL;
		} else if ("bold-italic".equals(tok)) {
			style = FontStyle.ITALIC;
			weight = FontWeight.BOLD;
		} else {
			lexer.reportError("font style expected, \"" + tok + "\" seen");
		}
		size = lexer.getInt();
		return new FontSpec(name, style, weight, size);
	}

	private String parseFeatureName(boolean hasName) throws IOException {
		String name = null;
		if (hasName) {
			name = lexer.getString();
		}
		return name;
	}

	private BgImage parseBgImage(String tok) throws IOException {
		if (!"url".equals(tok)) {
			return null;
		}
		String url = lexer.getString();

		parseExpected("position");
		String position = lexer.getString();
		if (!("left-top".equals(position) || "left-center".equals(position)
				|| "left-bottom".equals(position)
				|| "right-top".equals(position)
				|| "right-center".equals(position)
				|| "right-bottom".equals(position)
				|| "center-top".equals(position)
				|| "center-bottom".equals(position) || "center-center"
				.equals(position))) {
			lexer.reportError("Image position expected!");
		}

		parseExpected("repeat");
		String repeat = lexer.getString();
		if (!("repeat".equals(repeat) || "repeat-x".equals(repeat)
				|| "repeat-y".equals(repeat) || "no-repeat".equals(repeat))) {
			lexer.reportError("Image repeat expected!");
		}
		return new BgImage(url, position, repeat); //
	}

	private Rectangle parseRectangle() throws IOException {
		parseExpected("(");
		return parseRectangleNoLeading();
	}

	private Rectangle parseRectangleNoLeading() throws IOException {
		int x1 = lexer.getInt();
		int y1 = lexer.getInt();
		int x2 = lexer.getInt();
		int y2 = lexer.getInt();
		if (x2 < x1 || y2 < y1) {
			lexer.reportError("Second coordinates must be lower-right corner");
		}
		parseExpected(")");
		return new Rectangle(x1, y1, 1 + x2 - x1, 1 + y2 - y1);
	}

	private void resolveModifier(final SEModifier m, final SubFeature part) {
		ForwardReference fw = new ForwardReference(lexer) {
			public void resolve() throws IOException {
				m.setPart(part.getFeature());
			}
		};
		deferred.get(0).add(fw);
	}

	private void finishBuilding() throws IOException {
		for (int i = 0; i < deferred.size(); i++) {
			ArrayList<ForwardReference> list = deferred.get(i);
			for (int j = 0; j < list.size(); j++) {
				ForwardReference fw = list.get(j);
				fw.resolveAtLine();
			}
		}

		builder.finishBuilding();
		if (EasingEquation.framesAdded > 0) {
			System.out.println(EasingEquation.framesAdded
					+ " animation keyframes added for easing functions.");
		}
	}

	// *************** Convenience Methods ******************

	private SubFeature[] parsePartsList() throws IOException {
		parseExpected("{");
		ArrayList<SubFeature> partsList = new ArrayList<SubFeature>();
		for (;;) {
			String tok = lexer.getString();
			if ("}".equals(tok)) {
				break;
			}
			partsList.add(parseSubFeature(tok));
		}
		return partsList.toArray(new SubFeature[partsList.size()]);
	}

	private SubFeature parseSubFeature(String tok) throws IOException {
		if ("}".equals(tok)) {
			lexer.reportError("\"}\" unexpected");
		} else if (tok == null) {
			lexer.reportError("Unexpected EOF");
		} else if ("sub_feature".equals(tok)) {
			// inline feature
			return new SubFeature(parseFeature(false, lexer.getLineNumber()),
					lexer);
		} else {
			// reference to named feature
			return new SubFeature(builder, tok, lexer);
		}
		return null; // not reached
	}

	private SEFeature[] makeFeatureList(SubFeature[] parts) throws IOException {
		SEFeature[] result = new SEFeature[parts.length];
		for (int i = 0; i < parts.length; i++) {
			result[i] = parts[i].getFeature();
		}
		return result;
	}

	private SERCHandler[] makeRCHandlerList(String[] names) throws IOException {
		SERCHandler[] result = new SERCHandler[names.length];
		for (int i = 0; i < names.length; i++) {
			result[i] = builder.getNamedRCHandler(names[i]);
			if (result[i] == null) {
				lexer.reportError("RC handler \"" + names[i] + "\" not found");
			}
		}
		return result;
	}

	/**
	 * Look up the parts of an assembly. This can be called from a forward
	 * reference.
	 * 
	 * @see ForwardReference
	 **/
	public SEFeature[] lookupAssemblyParts(SEAssembly assembly, String[] parts)
			throws IOException {
		if (parts == null) {
			return null;
		}
		SEFeature[] result = new SEFeature[parts.length];
		for (int i = 0; i < parts.length; i++) {
			if (parts[i] == null) {
				result[i] = null;
			} else {
				result[i] = assembly.findPart(parts[i]);
				if (result[i] == null) {
					lexer.reportError("Assembly part \"" + parts[i]
							+ "\" not found");
				}
			}
		}
		return result;
	}

	/**
	 * Look up a named feature. Fail with an IOException if it's not found. This
	 * can be called from a forward reference.
	 * 
	 * @see ForwardReference
	 **/
	public SEFeature lookupFeatureOrFail(String name) throws IOException {
		SEFeature f = builder.getNamedFeature(name);
		if (f == null) {
			lexer.reportError("Feature " + name + " not found,");
		}
		return f;
	}

	/**
	 * Look up a named assembly. Fail with an {@code IOException} it it's not
	 * found. This can becalled from a forward reference.
	 * 
	 * @param name
	 * @return
	 * @throws IOException
	 */
	public SEAssembly lookupAssemblyOrFail(String name) throws IOException {
		SEFeature f = lookupFeatureOrFail(name);
		if (f == null || !(f instanceof SEAssembly)) {
			lexer.reportError("Feature " + name + " is not an assembly");
		}
		return (SEAssembly) f;
	}

	// *************** BASIC TYPES ************************

	/**
	 * Parse a list of strings
	 **/
	public String[] parseStrings() throws IOException {
		parseExpected("{");
		return parseStringsWithOpenBraceRead();
	}

	/**
	 * Parse a list of strings, without reading the leading open-brace.
	 **/
	public String[] parseStringsWithOpenBraceRead() throws IOException {
		Vector<String> v = new Vector<String>();
		for (;;) {
			String tok = lexer.getString();
			if (tok == null) {
				lexer.reportError("EOF unexpected in string list");
			} else if ("}".equals(tok)) {
				break;
			} else {
				v.addElement(tok);
			}
		}
		int num = v.size();
		String[] result = new String[num];
		for (int i = 0; i < num; i++) {
			result[i] = (String) v.elementAt(i);
		}
		return result;
	}

	/**
	 * Parse a boolean value
	 **/
	public boolean parseBoolean() throws IOException {
		String tok = lexer.getString();
		if ("true".equals(tok)) {
			return true;
		} else if ("false".equals(tok)) {
			return false;
		} else {
			lexer.reportError("\"true\" or \"false\" expected, \"" + tok
					+ "\" seen");
			return false;
		}
	}

	/**
	 * Parse a color representation ("{ r g b a }")
	 **/
	public Color parseColor() throws IOException {
		parseExpected("{");
		return parseColorNoOpenBrace();
	}

	/**
	 * Parse a color representation when the opening brace has already been
	 * read.
	 * 
	 * @see #parseColor()
	 **/
	public Color parseColorNoOpenBrace() throws IOException {
		int r = lexer.getInt();
		checkColorValue("r", r);
		int g = lexer.getInt();
		checkColorValue("g", g);
		int b = lexer.getInt();
		checkColorValue("b", b);
		int a = lexer.getInt();
		checkColorValue("a", a);
		parseExpected("}");
		return new Color(r, g, b, a);
	}

	private void checkColorValue(String name, int value) throws IOException {
		if (value < 0 || value > 255) {
			throw new IOException("Illegal color value for " + name + ":  "
					+ value);
		}
	}

	/**
	 * Parses a token that we expect to see. A token is read, and if it's not
	 * the expected token, an IOException is generated. This can be useful for
	 * things like parsing the ";" at the end of various constructs.
	 **/
	public void parseExpected(String expected) throws IOException {
		lexer.parseExpected(expected);
	}

	/**
	 * Read a java_command fragment. This is terminated with a "]]", which is
	 * consumed. This recognizes the special sequence GRIN_COMMAND_[[ f ]].
	 * 
	 * @param javaSource
	 * @throws IOException
	 */
	public void readJavaSource(StringBuffer javaSource) throws IOException {
		for (;;) {
			String src = lexer.getStringExact();
			if (src == null) {
				lexer.reportError("EOF unexpected");
			} else if ("]]".equals(src)) {
				return;
			} else if ("GRIN_COMMAND_[[".equals(src)) {
				// TODO
			}
			javaSource.append(src);
		}
	}

}